// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2016-2019, LomboTech Co.Ltd.
 * Authors:
 *	lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute  it and/or modify it
 * under  the terms of  the GNU General  Public License as published by the
 * Free Software Foundation;  either version 2 of the  License, or (at your
 * option) any later version.
 *
 */

#include <drm/drmP.h>
#include <drm/drm_atomic_helper.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_fb_helper.h>
#include <drm/drm_gem_cma_helper.h>
#include <drm/drm_gem_framebuffer_helper.h>

#include "lombo_crtc.h"
#include "lombo_dc.h"
#include "lombo_drv.h"
#include "lombo_fb_sc.h"
#include "lombo_fb_array.h"
#include "lombo_crtc.h"
#include "lombo_dc.h"

static struct drm_mode_config_helper_funcs lombo_mode_config_helpers = {
	.atomic_commit_tail	= drm_atomic_helper_commit_tail_rpm,
};

static struct drm_framebuffer *
lombo_fb_create(struct drm_device *dev, struct drm_file *file,
	const struct drm_mode_fb_cmd2 *mode_cmd)
{
	if (mode_cmd->flags & DRM_MODE_FB_MODIFIERS) {
		DRM_DEBUG_KMS("modifier=0x%llx\n", mode_cmd->modifier[0]);
		if (mode_cmd->modifier[0] == DRM_FORMAT_MOD_LOMBO_SC)
			return lombo_fb_sc_create(dev, file, mode_cmd);
		else if (mode_cmd->modifier[0] == DRM_FORMAT_MOD_LOMBO_FB_ARRAY)
			return lombo_fb_array_create(dev, file, mode_cmd);
		return ERR_PTR(-EINVAL);
	}

	return drm_gem_fb_create(dev, file, mode_cmd);
}

static void lombo_fb_output_poll_changed(struct drm_device *dev)
{
	DRM_DEBUG_KMS("lombo fb output poll changed:%d\n", __LINE__);
	drm_fb_helper_hotplug_event(dev->fb_helper);
}

static int lombo_mode_atomic_check(
	struct drm_device *dev, struct drm_atomic_state *state)
{
	int ret;

	DRM_DEBUG_ATOMIC("%s:%d\n", __func__, __LINE__);

	ret = drm_atomic_helper_check_modeset(dev, state);
	if (ret)
		return ret;

	ret = drm_atomic_normalize_zpos(dev, state);
	if (ret)
		return ret;

	return drm_atomic_helper_check_planes(dev, state);
}

static const struct drm_mode_config_funcs lombo_mode_config_funcs = {
	.fb_create		= lombo_fb_create,
	.output_poll_changed	= lombo_fb_output_poll_changed,
	.atomic_check		= lombo_mode_atomic_check,
	.atomic_commit		= drm_atomic_helper_commit,
};

static bool connector_in_pipeline(struct drm_connector *connector,
	struct lombo_pipeline *pipeline)
{
	int i;

	for (i = 0; i < pipeline->output_dev_cnt; i++) {
		DRM_DEBUG_DRIVER("connector name[%s] pipeline con name[%s]\n",
				connector->name, pipeline->con[i]->name);

		if (connector == pipeline->con[i])
			return true;
	}
	return false;
}

static struct drm_display_mode *lombo_get_top_mode(
	struct drm_fb_helper_connector *fb_connector, int width, int height)
{
	struct drm_display_mode *mode;

	list_for_each_entry(mode, &fb_connector->connector->modes, head) {
		if (mode->hdisplay > width || mode->vdisplay > height)
			continue;
		return mode;
	}
	return NULL;
}

bool lombo_fbdev_initial_config(struct drm_fb_helper *fb_helper,
			  struct drm_fb_helper_crtc **crtcs,
			  struct drm_display_mode **modes,
			  struct drm_fb_offset *offsets,
			  bool *enabled, int width, int height)
{
	bool share_fbdev = false;
	int i, j, fb_idx;
	struct drm_fb_helper_crtc *fb_helper_crtc = NULL;
	struct drm_connector *connector;
	struct drm_fb_helper_connector *fb_helper_conn;
	struct lombo_drv *drv = fb_helper->dev->dev_private;
	struct lombo_pipeline *pipeline = NULL;

	if ((drv->fbdev_cnt == 1) && (fb_helper->crtc_count == 2))
		share_fbdev = true;

	for (i = 0; i < FB_MAX; i++)
		if (!registered_fb[i])
			break;
	fb_idx = i;
	DRM_DEBUG_DRIVER("fbdev[%d] initial config\n", fb_idx);

	for (i = 0; i < fb_helper->crtc_count; i++) {
		if (drm_crtc_index(fb_helper->crtc_info[i].mode_set.crtc) ==
				fb_idx) {
			fb_helper_crtc = &fb_helper->crtc_info[i];
			break;
		}
	}

	if (!fb_helper_crtc) {
		DRM_ERROR("not found any crtc_info for fb%i\n", fb_idx);
		return false;
	}
	pipeline = &drv->pipeline[fb_idx];

	memset(modes, 0, fb_helper->connector_count*sizeof(modes[0]));
	memset(crtcs, 0, fb_helper->connector_count*sizeof(crtcs[0]));
	memset(offsets, 0, fb_helper->connector_count*sizeof(offsets[0]));

	for (i = 0; i < fb_helper->connector_count; i++) {
		fb_helper_conn = fb_helper->connector_info[i];

		modes[i] = drm_has_preferred_mode(fb_helper_conn,
							width, height);
		if (modes[i])
			continue;
		modes[i] = lombo_get_top_mode(fb_helper_conn, width, height);
		if (modes[i])
			continue;
	}

	j = 0;
	for (i = 0; i < fb_helper->connector_count; i++) {
		fb_helper_conn = fb_helper->connector_info[i];
		connector = fb_helper_conn->connector;

		if (!connector_in_pipeline(connector, pipeline)) {
			DRM_DEBUG_DRIVER("%s not in pipeline\n",
				connector->name);
			crtcs[i] = NULL;
			continue;
		}
		if (share_fbdev) {
			fb_helper_crtc = &fb_helper->crtc_info[j++];
			if (!fb_helper_crtc)
				DRM_DEBUG_DRIVER("no crtc_info for %s\n",
					connector->name);
		}
		crtcs[i] = fb_helper_crtc;
		DRM_DEBUG_DRIVER("found %s on %s to %p\n", modes[i]->name,
			connector->name, crtcs[i]);
	}

	return true;
}

static const struct drm_fb_helper_funcs lombo_fb_cma_helper_funcs = {
	.fb_probe = drm_fb_helper_generic_probe,
	.initial_config = lombo_fbdev_initial_config,
};

#ifdef CONFIG_DRM_FBDEV_EMULATION

static inline bool __valid_format_for_copy(
	uint32_t src_format, uint32_t dst_format)
{
	return (src_format == dst_format) ||
		((src_format == DRM_FORMAT_RGB888) &&
		 (dst_format == DRM_FORMAT_ARGB8888)) ||
		((src_format == DRM_FORMAT_ARGB8888) &&
		 (dst_format == DRM_FORMAT_ARGB1555));
}

static inline void __line_to_line(char *pdst, char *psrc, char *psrc_end,
	uint32_t dst_pitch, uint32_t src_pitch, uint32_t copy_bytes)
{
	for (; psrc != psrc_end; psrc += src_pitch) {
		memcpy((void *)pdst, (void *)psrc, copy_bytes);
		pdst += dst_pitch;
	}
}

static inline void __rg24_to_ar24(char *pdst, char *psrc, char *psrc_end,
	uint32_t dst_pitch, uint32_t src_pitch, uint32_t copy_width)
{
	struct _rg24_struct {
		uint16_t gb;
		uint8_t r;
	} __packed;

	for (; psrc != psrc_end; psrc += src_pitch) {
		struct _rg24_struct *p = (struct _rg24_struct *)psrc;
		struct _rg24_struct *p_end = p + copy_width;
		uint32_t *d = (uint32_t *)pdst;

		for (; p != p_end; p++)
			*d++ = 0xFF000000 | (p->r << 16) | p->gb;

		pdst += dst_pitch;
	}
}

static inline void __ar24_to_ar15(char *pdst, char *psrc, char *psrc_end,
	uint32_t dst_pitch, uint32_t src_pitch, uint32_t copy_width)
{
	for (; psrc != psrc_end; psrc += src_pitch) {
		uint32_t *p = (uint32_t *)psrc;
		uint32_t *p_end = p + copy_width;
		uint16_t *d = (uint16_t *)pdst;

		for (; p != p_end; p++)
			*d++ = ((*p >> 16) & 0x8000) |
				((*p >> 9) & 0x7C00) |
				((*p >> 6) & 0x3E0) |
				((*p >> 3) & 0x1F);

		pdst += dst_pitch;
	}
}

static int lombo_fbdev_copy_boot_fb(struct drm_fb_helper *fb_helper,
	struct lombo_boot_disp *bd)
{
	struct drm_framebuffer *fb;
	const struct drm_format_info *src_fmt;
	const struct drm_format_info *dst_fmt;
	struct drm_gem_cma_object *cma_obj;
	uint32_t fb_height;

	char *map_vaddr, *psrc, *psrc_end, *pdst;
	uint32_t map_offset;
	uint32_t size;
	int ret;

	if (!bd || !bd->phy_addr || !bd->pitch) {
		DRM_DEBUG("no boot fb\n");
		return -EINVAL;
	}
	src_fmt = drm_format_info(bd->fmt);
	if (src_fmt == NULL) {
		DRM_INFO("%s: drm_format_info fail\n",
			__func__);
		return -EINVAL;
	}

	if (!(fb_helper && fb_helper->fb)) {
		DRM_INFO("%s: fb_helper=%p,fb=%p\n", __func__,
			fb_helper, fb_helper ? fb_helper->fb : NULL);
		return -EINVAL;
	}
	fb = fb_helper->fb;
#ifdef CONFIG_DRM_FBDEV_OVERALLOC
	fb_height = fb->height * 100 / CONFIG_DRM_FBDEV_OVERALLOC;
#else
	fb_height = fb->height;
#endif
	dst_fmt = fb->format;

	if (!__valid_format_for_copy(src_fmt->format, dst_fmt->format)) {
		DRM_INFO("%s: no match pixel format %x %x\n",
			__func__, src_fmt->format, dst_fmt->format);
		return -EINVAL;
	}

	/* kmap boot fb */
	size = bd->pitch * bd->height;
	ret = lombo_fb_kmap(bd->phy_addr, size, 1,
			(void **)&map_vaddr, &map_offset);
	if (ret) {
		DRM_INFO("map boot fb fail, ret=%d\n", ret);
		return ret;
	}

	/* fixup copy region */
	if (bd->copy_w > fb->width) {
		bd->copy_x += (bd->copy_w - fb->width) >> 1;
		bd->copy_w = fb->width;
		DRM_DEBUG("fixup copyregion: x=%d, w=%d\n",
			bd->copy_x, bd->copy_w);
	}
	if (bd->copy_h > fb_height) {
		bd->copy_y += (bd->copy_h - fb_height) >> 1;
		bd->copy_h = fb_height;
		DRM_DEBUG("fixup copyregion: y=%d, h=%d\n",
			bd->copy_y, bd->copy_h);
	}

	/* calc vaddr of src */
	psrc = map_vaddr + map_offset +
		bd->copy_y * bd->pitch +
		bd->copy_x * src_fmt->cpp[0];
	psrc_end = psrc + bd->copy_h * bd->pitch;

	/* calc vaddr of dst */
	cma_obj = drm_fb_cma_get_gem_obj(fb, 0);
	pdst = (char *)(cma_obj->vaddr) + fb->offsets[0] +
		((fb_height - bd->copy_h) >> 1) * fb->pitches[0] +
		((fb->width - bd->copy_w) >> 1) * dst_fmt->cpp[0];

	DRM_DEBUG("map:vaddr=%p,ofs=%u;psrc=%p,_end=%p,cmavaddr=%p,pdst=%p\n",
		map_vaddr, map_offset, psrc, psrc_end,
		cma_obj->vaddr, pdst);

	/* do copy boot_fb */
	if (src_fmt->format == dst_fmt->format)
		__line_to_line(pdst, psrc, psrc_end,
			       fb->pitches[0], bd->pitch,
			       bd->copy_w * src_fmt->cpp[0]);
	else if ((src_fmt->format == DRM_FORMAT_RGB888) &&
		 (dst_fmt->format == DRM_FORMAT_ARGB8888))
		__rg24_to_ar24(pdst, psrc, psrc_end,
			       fb->pitches[0], bd->pitch,
			       bd->copy_w);
	else if ((src_fmt->format == DRM_FORMAT_ARGB8888) &&
		 (dst_fmt->format == DRM_FORMAT_ARGB1555))
		__ar24_to_ar15(pdst, psrc, psrc_end,
			       fb->pitches[0], bd->pitch,
			       bd->copy_w);
	else
		DRM_INFO("fixme copy\n");

	lombo_fb_kunmap((void **)&map_vaddr);
	return 0;
}

static int lombo_fb_helper_init(struct drm_device *dev,
		       struct drm_fb_helper *fb_helper,
		       int max_conn_count)
{
	struct drm_crtc *crtc;
	struct drm_mode_config *config = &dev->mode_config;
	int i, j, fb_idx = 0;
	struct lombo_drv *drv = dev->dev_private;
	bool share_fbdev = false;

	if (!max_conn_count)
		return -EINVAL;

	if ((drv->fbdev_cnt == 1) && (fb_helper->crtc_count == 2))
		share_fbdev = true;

	if (share_fbdev) {
		fb_helper->crtc_count = config->num_crtc;
	} else {
		fb_helper->crtc_count = 1;
		for (i = 0 ; i < FB_MAX; i++)
			if (!registered_fb[i])
				break;
		fb_idx = i;
	}

	DRM_DEBUG_KMS("%s initial fbdev[%d]\n", __func__, fb_idx);

	fb_helper->crtc_info = kcalloc(fb_helper->crtc_count,
			sizeof(struct drm_fb_helper_crtc), GFP_KERNEL);
	if (!fb_helper->crtc_info)
		return -ENOMEM;

	fb_helper->connector_info = kcalloc(dev->mode_config.num_connector,
			sizeof(struct drm_fb_helper_connector *), GFP_KERNEL);
	if (!fb_helper->connector_info) {
		kfree(fb_helper->crtc_info);
		return -ENOMEM;
	}
	fb_helper->connector_info_alloc_count = dev->mode_config.num_connector;
	fb_helper->connector_count = 0;

	for (i = 0; i < fb_helper->crtc_count; i++) {
		fb_helper->crtc_info[i].mode_set.connectors =
			kcalloc(max_conn_count,
				sizeof(struct drm_connector *),
				GFP_KERNEL);

		if (!fb_helper->crtc_info[i].mode_set.connectors)
			goto out_free;
		fb_helper->crtc_info[i].mode_set.num_connectors = 0;
		fb_helper->crtc_info[i].rotation = DRM_MODE_ROTATE_0;
	}

	if (share_fbdev) {
		i = 0;
		drm_for_each_crtc(crtc, dev) {
			fb_helper->crtc_info[i].mode_set.crtc = crtc;
			i++;
		}
	} else {
		drm_for_each_crtc(crtc, dev)
			if (fb_idx == drm_crtc_index(crtc))
				fb_helper->crtc_info->mode_set.crtc = crtc;
	}
	dev->fb_helper = fb_helper;

	return 0;
out_free:
	for (i = 0; i < fb_helper->connector_count; i++) {
		drm_connector_put(fb_helper->connector_info[i]->connector);
		kfree(fb_helper->connector_info[i]);
	}
	kfree(fb_helper->connector_info);

	for (i = 0; i < fb_helper->crtc_count; i++) {
		struct drm_mode_set *modeset =
					&fb_helper->crtc_info[i].mode_set;

		for (j = 0; j < modeset->num_connectors; j++) {
			drm_connector_put(modeset->connectors[j]);
			modeset->connectors[j] = NULL;
		}
		modeset->num_connectors = 0;

		drm_mode_destroy(fb_helper->dev, modeset->mode);
		modeset->mode = NULL;
		modeset->fb = NULL;
		kfree(modeset->connectors);
	}
	kfree(fb_helper->crtc_info);

	return -ENOMEM;
}
#else
static inline int lombo_fb_helper_init(struct drm_device *dev,
		     struct drm_fb_helper *helper,
		     int max_conn)
{
	/* So drivers can use it to free the struct */
	helper->dev = dev;
	dev->fb_helper = helper;

	return 0;
}
#endif

struct drm_fb_helper *lombo_fbdev_cma_init(struct drm_device *dev,
	unsigned int preferred_bpp, unsigned int max_conn_count)
{
	struct drm_fb_helper *fb_helper;
	int ret;

	fb_helper = kzalloc(sizeof(*fb_helper), GFP_KERNEL);
	if (!fb_helper)
		return ERR_PTR(-ENOMEM);

	ret = drm_client_init(dev, &fb_helper->client, "fbdev", NULL);
	if (ret) {
		DRM_ERROR("drm_client_init fail ret[%d]\n", ret);
		goto err_free;
	}

	if (!preferred_bpp)
		preferred_bpp = dev->mode_config.preferred_depth;
	if (!preferred_bpp)
		preferred_bpp = 32;

	if (!max_conn_count)
		max_conn_count = dev->mode_config.num_connector;
	if (!max_conn_count) {
		DRM_DEV_ERROR(dev->dev, "No connectors\n");
		goto err_client_put;
	}

	drm_fb_helper_prepare(dev, fb_helper, &lombo_fb_cma_helper_funcs);

	ret = lombo_fb_helper_init(dev, fb_helper, max_conn_count);
	if (ret < 0) {
		DRM_DEV_ERROR(dev->dev, "Failed to initialize fbdev helper\n");
		goto err_client_put;
	}

	ret = drm_fb_helper_single_add_all_connectors(fb_helper);
	if (ret < 0) {
		DRM_DEV_ERROR(dev->dev, "Failed to add connectors\n");
		goto err_drm_fb_helper_fini;
	}

	ret = drm_fb_helper_initial_config(fb_helper, preferred_bpp);
	if (ret < 0) {
		DRM_DEV_ERROR(dev->dev, "Failed to set fbdev configuration\n");
		goto err_drm_fb_helper_fini;
	}

	drm_client_add(&fb_helper->client);

	return fb_helper;
err_drm_fb_helper_fini:
	drm_fb_helper_fbdev_teardown(dev);
err_client_put:
	drm_client_release(&fb_helper->client);
err_free:
	kfree(fb_helper);

	return ERR_PTR(ret);
}
EXPORT_SYMBOL(lombo_fbdev_cma_init);

int lombo_framebuffer_init(struct drm_device *drm)
{
	int i;
	struct lombo_drv *drv = drm->dev_private;

	DRM_DEV_DEBUG_DRIVER(drm->dev, "\n");

	drm_mode_config_reset(drm);
	drm->mode_config.max_width = 4096;
	drm->mode_config.max_height = 4096;
	drm->mode_config.funcs = &lombo_mode_config_funcs;
	drm->mode_config.helper_private = &lombo_mode_config_helpers;
	drm->mode_config.allow_fb_modifiers = true;

	drv->fbdev = devm_kcalloc(drm->dev, drm->mode_config.num_crtc,
			sizeof(*drv->fbdev), GFP_KERNEL);
	if (!drv->fbdev) {
		DRM_DEV_ERROR(drm->dev, "alloc pfbdev fail\n");
		return -ENOMEM;
	}

	for (i = 0; i < drv->fbdev_cnt; i++) {
#ifdef CONFIG_DRM_FBDEV_EMULATION
		DRM_DEBUG_DRIVER("inittial fbdev[%d]", i);
		drv->fbdev[i] = lombo_fbdev_cma_init(drm,
			drv->fb_bpp[i] ? drv->fb_bpp[i] : 32, 0);
		if (IS_ERR(drv->fbdev[i])) {
			DRM_DEV_ERROR(drm->dev, "init fbdev cma fail\n");
			return PTR_ERR(drv->fbdev[i]);
		}
		if (drv->fbdev[i])
			lombo_fbdev_copy_boot_fb(
				(struct drm_fb_helper *)drv->fbdev[i],
				drv->boot_disp);
#else
		drv->fbdev[i] = NULL;
#endif
	}

	return 0;
}

void lombo_framebuffer_free(struct drm_device *drm)
{
	int i;
	struct lombo_drv *drv = drm->dev_private;

	for (i = 0; i < drv->fbdev_cnt; i++)
		drm_fb_helper_unregister_fbi(drv->fbdev[i]);

	drm->mode_config.funcs = NULL;
}

void lombo_framebuffer_restore_fbdev(struct drm_device *drm,
	struct drm_fb_helper *fb_helper)
{
	DRM_DEV_DEBUG_DRIVER(drm->dev, "fixme\n");
	drm_fb_helper_restore_fbdev_mode_unlocked(fb_helper);
}

int lombo_fb_probe_actual_fbdev(struct drm_fb_helper *fb_helper)
{
	struct drm_device *dev;
	struct drm_crtc *crtc;
	struct lombo_crtc *ecrtc;
	int ret = 0;

	if (!fb_helper) {
		DRM_WARN("null fb_helper\n");
		return -EINVAL;
	}
	dev = fb_helper->dev;

	list_for_each_entry(crtc, &dev->mode_config.crtc_list, head) {
		if (crtc && crtc == fb_helper->crtc_info->mode_set.crtc) {
			ecrtc = to_lombo_crtc(crtc);
			ret = lombo_dc_probe_fbdev(ecrtc->dc, fb_helper);
			break;
		}
	}

	if (ret)
		DRM_ERROR("probe actual fbdev failed, ret=%d\n", ret);

	return ret;
}

int lombo_fb_set_transparent_rect(struct fb_info *info,
	struct drm_lombo_rect_wh *rect)
{
	struct drm_fb_helper *fb_helper = info->par;
	struct drm_device *dev = fb_helper->dev;
	struct drm_crtc *crtc;
	struct lombo_crtc *ecrtc;
	int ret = 0;

	drm_modeset_lock_all(dev);
	list_for_each_entry(crtc, &dev->mode_config.crtc_list, head) {
		if (crtc == fb_helper->crtc_info->mode_set.crtc) {
			ecrtc = to_lombo_crtc(crtc);
			ret = lombo_dc_set_fb_transparent_rect(ecrtc->dc,
							       fb_helper->fb,
							       rect);
			break; /* only set a rect */
		}
	}
	drm_modeset_unlock_all(dev);

	return ret;
}

int lombo_fb_get_transparent_rect(struct fb_info *info,
	struct drm_lombo_rect_wh *rect)
{
	struct drm_fb_helper *fb_helper = info->par;
	struct drm_device *dev = fb_helper->dev;
	struct drm_crtc *crtc;
	struct lombo_crtc *ecrtc;
	int ret = -ENODATA;

	list_for_each_entry(crtc, &dev->mode_config.crtc_list, head) {
		if (crtc == fb_helper->crtc_info->mode_set.crtc) {
			ecrtc = to_lombo_crtc(crtc);
			ret = lombo_dc_get_fb_transparent_rect(ecrtc->dc,
							       fb_helper->fb,
							       rect);
			break; /* only get a rect */
		}
	}
	return ret;
}

